/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

var zrUtil = require("static/plugins/js/zrender/lib/core/util");

var graphic = require("../../util/graphic");

var SymbolClz = require("../helper/Symbol");

var _layoutHelper = require("./layoutHelper");

var radialCoordinate = _layoutHelper.radialCoordinate;

var echarts = require("../../echarts");

var bbox = require("static/plugins/js/zrender/lib/core/bbox");

var View = require("../../coord/View");

var roamHelper = require("../../component/helper/roamHelper");

var RoamController = require("../../component/helper/RoamController");

var _cursorHelper = require("../../component/helper/cursorHelper");

var onIrrelevantElement = _cursorHelper.onIrrelevantElement;

var _config = require("../../config");

var __DEV__ = _config.__DEV__;

var _number = require("../../util/number");

var parsePercent = _number.parsePercent;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
var TreeShape = graphic.extendShape({
    shape: {
        parentPoint: [],
        childPoints: [],
        orient: "",
        forkPosition: "",
    },
    style: {
        stroke: "#000",
        fill: null,
    },
    buildPath: function (ctx, shape) {
        var childPoints = shape.childPoints;
        var childLen = childPoints.length;
        var parentPoint = shape.parentPoint;
        var firstChildPos = childPoints[0];
        var lastChildPos = childPoints[childLen - 1];

        if (childLen === 1) {
            ctx.moveTo(parentPoint[0], parentPoint[1]);
            ctx.lineTo(firstChildPos[0], firstChildPos[1]);
            return;
        }

        var orient = shape.orient;
        var forkDim = orient === "TB" || orient === "BT" ? 0 : 1;
        var otherDim = 1 - forkDim;
        var forkPosition = parsePercent(shape.forkPosition, 1);
        var tmpPoint = [];
        tmpPoint[forkDim] = parentPoint[forkDim];
        tmpPoint[otherDim] =
            parentPoint[otherDim] +
            (lastChildPos[otherDim] - parentPoint[otherDim]) * forkPosition;
        ctx.moveTo(parentPoint[0], parentPoint[1]);
        ctx.lineTo(tmpPoint[0], tmpPoint[1]);
        ctx.moveTo(firstChildPos[0], firstChildPos[1]);
        tmpPoint[forkDim] = firstChildPos[forkDim];
        ctx.lineTo(tmpPoint[0], tmpPoint[1]);
        tmpPoint[forkDim] = lastChildPos[forkDim];
        ctx.lineTo(tmpPoint[0], tmpPoint[1]);
        ctx.lineTo(lastChildPos[0], lastChildPos[1]);

        for (var i = 1; i < childLen - 1; i++) {
            var point = childPoints[i];
            ctx.moveTo(point[0], point[1]);
            tmpPoint[forkDim] = point[forkDim];
            ctx.lineTo(tmpPoint[0], tmpPoint[1]);
        }
    },
});

var _default = echarts.extendChartView({
    type: "tree",

    /**
     * Init the chart
     * @override
     * @param  {module:echarts/model/Global} ecModel
     * @param  {module:echarts/ExtensionAPI} api
     */
    init: function (ecModel, api) {
        /**
         * @private
         * @type {module:echarts/data/Tree}
         */
        this._oldTree;
        /**
         * @private
         * @type {module:zrender/container/Group}
         */

        this._mainGroup = new graphic.Group();
        /**
         * @private
         * @type {module:echarts/componet/helper/RoamController}
         */

        this._controller = new RoamController(api.getZr());
        this._controllerHost = {
            target: this.group,
        };
        this.group.add(this._mainGroup);
    },
    render: function (seriesModel, ecModel, api, payload) {
        var data = seriesModel.getData();
        var layoutInfo = seriesModel.layoutInfo;
        var group = this._mainGroup;
        var layout = seriesModel.get("layout");

        if (layout === "radial") {
            group.attr("position", [
                layoutInfo.x + layoutInfo.width / 2,
                layoutInfo.y + layoutInfo.height / 2,
            ]);
        } else {
            group.attr("position", [layoutInfo.x, layoutInfo.y]);
        }

        this._updateViewCoordSys(seriesModel, layoutInfo, layout);

        this._updateController(seriesModel, ecModel, api);

        var oldData = this._data;
        var seriesScope = {
            expandAndCollapse: seriesModel.get("expandAndCollapse"),
            layout: layout,
            edgeShape: seriesModel.get("edgeShape"),
            edgeForkPosition: seriesModel.get("edgeForkPosition"),
            orient: seriesModel.getOrient(),
            curvature: seriesModel.get("lineStyle.curveness"),
            symbolRotate: seriesModel.get("symbolRotate"),
            symbolOffset: seriesModel.get("symbolOffset"),
            hoverAnimation: seriesModel.get("hoverAnimation"),
            useNameLabel: true,
            fadeIn: true,
        };
        data.diff(oldData)
            .add(function (newIdx) {
                if (symbolNeedsDraw(data, newIdx)) {
                    // Create node and edge
                    updateNode(
                        data,
                        newIdx,
                        null,
                        group,
                        seriesModel,
                        seriesScope
                    );
                }
            })
            .update(function (newIdx, oldIdx) {
                var symbolEl = oldData.getItemGraphicEl(oldIdx);

                if (!symbolNeedsDraw(data, newIdx)) {
                    symbolEl &&
                        removeNode(
                            oldData,
                            oldIdx,
                            symbolEl,
                            group,
                            seriesModel,
                            seriesScope
                        );
                    return;
                } // Update node and edge

                updateNode(
                    data,
                    newIdx,
                    symbolEl,
                    group,
                    seriesModel,
                    seriesScope
                );
            })
            .remove(function (oldIdx) {
                var symbolEl = oldData.getItemGraphicEl(oldIdx); // When remove a collapsed node of subtree, since the collapsed
                // node haven't been initialized with a symbol element,
                // you can't found it's symbol element through index.
                // so if we want to remove the symbol element we should insure
                // that the symbol element is not null.

                if (symbolEl) {
                    removeNode(
                        oldData,
                        oldIdx,
                        symbolEl,
                        group,
                        seriesModel,
                        seriesScope
                    );
                }
            })
            .execute();
        this._nodeScaleRatio = seriesModel.get("nodeScaleRatio");

        this._updateNodeAndLinkScale(seriesModel);

        if (seriesScope.expandAndCollapse === true) {
            data.eachItemGraphicEl(function (el, dataIndex) {
                el.off("click").on("click", function () {
                    api.dispatchAction({
                        type: "treeExpandAndCollapse",
                        seriesId: seriesModel.id,
                        dataIndex: dataIndex,
                    });
                });
            });
        }

        this._data = data;
    },
    _updateViewCoordSys: function (seriesModel) {
        var data = seriesModel.getData();
        var points = [];
        data.each(function (idx) {
            var layout = data.getItemLayout(idx);

            if (layout && !isNaN(layout.x) && !isNaN(layout.y)) {
                points.push([+layout.x, +layout.y]);
            }
        });
        var min = [];
        var max = [];
        bbox.fromPoints(points, min, max); // If don't Store min max when collapse the root node after roam,
        // the root node will disappear.

        var oldMin = this._min;
        var oldMax = this._max; // If width or height is 0

        if (max[0] - min[0] === 0) {
            min[0] = oldMin ? oldMin[0] : min[0] - 1;
            max[0] = oldMax ? oldMax[0] : max[0] + 1;
        }

        if (max[1] - min[1] === 0) {
            min[1] = oldMin ? oldMin[1] : min[1] - 1;
            max[1] = oldMax ? oldMax[1] : max[1] + 1;
        }

        var viewCoordSys = (seriesModel.coordinateSystem = new View());
        viewCoordSys.zoomLimit = seriesModel.get("scaleLimit");
        viewCoordSys.setBoundingRect(
            min[0],
            min[1],
            max[0] - min[0],
            max[1] - min[1]
        );
        viewCoordSys.setCenter(seriesModel.get("center"));
        viewCoordSys.setZoom(seriesModel.get("zoom")); // Here we use viewCoordSys just for computing the 'position' and 'scale' of the group

        this.group.attr({
            position: viewCoordSys.position,
            scale: viewCoordSys.scale,
        });
        this._viewCoordSys = viewCoordSys;
        this._min = min;
        this._max = max;
    },
    _updateController: function (seriesModel, ecModel, api) {
        var controller = this._controller;
        var controllerHost = this._controllerHost;
        var group = this.group;
        controller.setPointerChecker(function (e, x, y) {
            var rect = group.getBoundingRect();
            rect.applyTransform(group.transform);
            return (
                rect.contain(x, y) && !onIrrelevantElement(e, api, seriesModel)
            );
        });
        controller.enable(seriesModel.get("roam"));
        controllerHost.zoomLimit = seriesModel.get("scaleLimit");
        controllerHost.zoom = seriesModel.coordinateSystem.getZoom();
        controller
            .off("pan")
            .off("zoom")
            .on(
                "pan",
                function (e) {
                    roamHelper.updateViewOnPan(controllerHost, e.dx, e.dy);
                    api.dispatchAction({
                        seriesId: seriesModel.id,
                        type: "treeRoam",
                        dx: e.dx,
                        dy: e.dy,
                    });
                },
                this
            )
            .on(
                "zoom",
                function (e) {
                    roamHelper.updateViewOnZoom(
                        controllerHost,
                        e.scale,
                        e.originX,
                        e.originY
                    );
                    api.dispatchAction({
                        seriesId: seriesModel.id,
                        type: "treeRoam",
                        zoom: e.scale,
                        originX: e.originX,
                        originY: e.originY,
                    });

                    this._updateNodeAndLinkScale(seriesModel);
                },
                this
            );
    },
    _updateNodeAndLinkScale: function (seriesModel) {
        var data = seriesModel.getData();

        var nodeScale = this._getNodeGlobalScale(seriesModel);

        var invScale = [nodeScale, nodeScale];
        data.eachItemGraphicEl(function (el, idx) {
            el.attr("scale", invScale);
        });
    },
    _getNodeGlobalScale: function (seriesModel) {
        var coordSys = seriesModel.coordinateSystem;

        if (coordSys.type !== "view") {
            return 1;
        }

        var nodeScaleRatio = this._nodeScaleRatio;
        var groupScale = coordSys.scale;
        var groupZoom = (groupScale && groupScale[0]) || 1; // Scale node when zoom changes

        var roamZoom = coordSys.getZoom();
        var nodeScale = (roamZoom - 1) * nodeScaleRatio + 1;
        return nodeScale / groupZoom;
    },
    dispose: function () {
        this._controller && this._controller.dispose();
        this._controllerHost = {};
    },
    remove: function () {
        this._mainGroup.removeAll();

        this._data = null;
    },
});

function symbolNeedsDraw(data, dataIndex) {
    var layout = data.getItemLayout(dataIndex);
    return (
        layout &&
        !isNaN(layout.x) &&
        !isNaN(layout.y) &&
        data.getItemVisual(dataIndex, "symbol") !== "none"
    );
}

function getTreeNodeStyle(node, itemModel, seriesScope) {
    seriesScope.itemModel = itemModel;
    seriesScope.itemStyle = itemModel.getModel("itemStyle").getItemStyle();
    seriesScope.hoverItemStyle = itemModel
        .getModel("emphasis.itemStyle")
        .getItemStyle();
    seriesScope.lineStyle = itemModel.getModel("lineStyle").getLineStyle();
    seriesScope.labelModel = itemModel.getModel("label");
    seriesScope.hoverLabelModel = itemModel.getModel("emphasis.label");

    if (node.isExpand === false && node.children.length !== 0) {
        seriesScope.symbolInnerColor = seriesScope.itemStyle.fill;
    } else {
        seriesScope.symbolInnerColor = "#fff";
    }

    return seriesScope;
}

function updateNode(
    data,
    dataIndex,
    symbolEl,
    group,
    seriesModel,
    seriesScope
) {
    var isInit = !symbolEl;
    var node = data.tree.getNodeByDataIndex(dataIndex);
    var itemModel = node.getModel();
    var seriesScope = getTreeNodeStyle(node, itemModel, seriesScope);
    var virtualRoot = data.tree.root;
    var source =
        node.parentNode === virtualRoot ? node : node.parentNode || node;
    var sourceSymbolEl = data.getItemGraphicEl(source.dataIndex);
    var sourceLayout = source.getLayout();
    var sourceOldLayout = sourceSymbolEl
        ? {
              x: sourceSymbolEl.position[0],
              y: sourceSymbolEl.position[1],
              rawX: sourceSymbolEl.__radialOldRawX,
              rawY: sourceSymbolEl.__radialOldRawY,
          }
        : sourceLayout;
    var targetLayout = node.getLayout();

    if (isInit) {
        symbolEl = new SymbolClz(data, dataIndex, seriesScope);
        symbolEl.attr("position", [sourceOldLayout.x, sourceOldLayout.y]);
    } else {
        symbolEl.updateData(data, dataIndex, seriesScope);
    }

    symbolEl.__radialOldRawX = symbolEl.__radialRawX;
    symbolEl.__radialOldRawY = symbolEl.__radialRawY;
    symbolEl.__radialRawX = targetLayout.rawX;
    symbolEl.__radialRawY = targetLayout.rawY;
    group.add(symbolEl);
    data.setItemGraphicEl(dataIndex, symbolEl);
    graphic.updateProps(
        symbolEl,
        {
            position: [targetLayout.x, targetLayout.y],
        },
        seriesModel
    );
    var symbolPath = symbolEl.getSymbolPath();

    if (seriesScope.layout === "radial") {
        var realRoot = virtualRoot.children[0];
        var rootLayout = realRoot.getLayout();
        var length = realRoot.children.length;
        var rad;
        var isLeft;

        if (targetLayout.x === rootLayout.x && node.isExpand === true) {
            var center = {};
            center.x =
                (realRoot.children[0].getLayout().x +
                    realRoot.children[length - 1].getLayout().x) /
                2;
            center.y =
                (realRoot.children[0].getLayout().y +
                    realRoot.children[length - 1].getLayout().y) /
                2;
            rad = Math.atan2(center.y - rootLayout.y, center.x - rootLayout.x);

            if (rad < 0) {
                rad = Math.PI * 2 + rad;
            }

            isLeft = center.x < rootLayout.x;

            if (isLeft) {
                rad = rad - Math.PI;
            }
        } else {
            rad = Math.atan2(
                targetLayout.y - rootLayout.y,
                targetLayout.x - rootLayout.x
            );

            if (rad < 0) {
                rad = Math.PI * 2 + rad;
            }

            if (
                node.children.length === 0 ||
                (node.children.length !== 0 && node.isExpand === false)
            ) {
                isLeft = targetLayout.x < rootLayout.x;

                if (isLeft) {
                    rad = rad - Math.PI;
                }
            } else {
                isLeft = targetLayout.x > rootLayout.x;

                if (!isLeft) {
                    rad = rad - Math.PI;
                }
            }
        }

        var textPosition = isLeft ? "left" : "right";
        var rotate = seriesScope.labelModel.get("rotate");
        var labelRotateRadian = rotate * (Math.PI / 180);
        symbolPath.setStyle({
            textPosition:
                seriesScope.labelModel.get("position") || textPosition,
            textRotation: rotate == null ? -rad : labelRotateRadian,
            textOrigin: "center",
            verticalAlign: "middle",
        });
    }

    drawEdge(
        seriesModel,
        node,
        virtualRoot,
        symbolEl,
        sourceOldLayout,
        sourceLayout,
        targetLayout,
        group,
        seriesScope
    );
}

function drawEdge(
    seriesModel,
    node,
    virtualRoot,
    symbolEl,
    sourceOldLayout,
    sourceLayout,
    targetLayout,
    group,
    seriesScope
) {
    var edgeShape = seriesScope.edgeShape;
    var edge = symbolEl.__edge;

    if (edgeShape === "curve") {
        if (node.parentNode && node.parentNode !== virtualRoot) {
            if (!edge) {
                edge = symbolEl.__edge = new graphic.BezierCurve({
                    shape: getEdgeShape(
                        seriesScope,
                        sourceOldLayout,
                        sourceOldLayout
                    ),
                    style: zrUtil.defaults(
                        {
                            opacity: 0,
                            strokeNoScale: true,
                        },
                        seriesScope.lineStyle
                    ),
                });
            }

            graphic.updateProps(
                edge,
                {
                    shape: getEdgeShape(
                        seriesScope,
                        sourceLayout,
                        targetLayout
                    ),
                    style: {
                        opacity: 1,
                    },
                },
                seriesModel
            );
        }
    } else if (edgeShape === "polyline") {
        if (seriesScope.layout === "orthogonal") {
            if (
                node !== virtualRoot &&
                node.children &&
                node.children.length !== 0 &&
                node.isExpand === true
            ) {
                var children = node.children;
                var childPoints = [];

                for (var i = 0; i < children.length; i++) {
                    var childLayout = children[i].getLayout();
                    childPoints.push([childLayout.x, childLayout.y]);
                }

                if (!edge) {
                    edge = symbolEl.__edge = new TreeShape({
                        shape: {
                            parentPoint: [targetLayout.x, targetLayout.y],
                            childPoints: [[targetLayout.x, targetLayout.y]],
                            orient: seriesScope.orient,
                            forkPosition: seriesScope.edgeForkPosition,
                        },
                        style: zrUtil.defaults(
                            {
                                opacity: 0,
                                strokeNoScale: true,
                            },
                            seriesScope.lineStyle
                        ),
                    });
                }

                graphic.updateProps(
                    edge,
                    {
                        shape: {
                            parentPoint: [targetLayout.x, targetLayout.y],
                            childPoints: childPoints,
                        },
                        style: {
                            opacity: 1,
                        },
                    },
                    seriesModel
                );
            }
        } else {
        }
    }

    group.add(edge);
}

function removeNode(
    data,
    dataIndex,
    symbolEl,
    group,
    seriesModel,
    seriesScope
) {
    var node = data.tree.getNodeByDataIndex(dataIndex);
    var virtualRoot = data.tree.root;
    var itemModel = node.getModel();
    var seriesScope = getTreeNodeStyle(node, itemModel, seriesScope);
    var source =
        node.parentNode === virtualRoot ? node : node.parentNode || node;
    var edgeShape = seriesScope.edgeShape;
    var sourceLayout;

    while (((sourceLayout = source.getLayout()), sourceLayout == null)) {
        source =
            source.parentNode === virtualRoot
                ? source
                : source.parentNode || source;
    }

    graphic.updateProps(
        symbolEl,
        {
            position: [sourceLayout.x + 1, sourceLayout.y + 1],
        },
        seriesModel,
        function () {
            group.remove(symbolEl);
            data.setItemGraphicEl(dataIndex, null);
        }
    );
    symbolEl.fadeOut(null, {
        keepLabel: true,
    });
    var sourceSymbolEl = data.getItemGraphicEl(source.dataIndex);
    var sourceEdge = sourceSymbolEl.__edge; // 1. when expand the sub tree, delete the children node should delete the edge of
    // the source at the same time. because the polyline edge shape is only owned by the source.
    // 2.when the node is the only children of the source, delete the node should delete the edge of
    // the source at the same time. the same reason as above.

    var edge =
        symbolEl.__edge ||
        (source.isExpand === false || source.children.length === 1
            ? sourceEdge
            : undefined);
    var edgeShape = seriesScope.edgeShape;

    if (edge) {
        if (edgeShape === "curve") {
            graphic.updateProps(
                edge,
                {
                    shape: getEdgeShape(
                        seriesScope,
                        sourceLayout,
                        sourceLayout
                    ),
                    style: {
                        opacity: 0,
                    },
                },
                seriesModel,
                function () {
                    group.remove(edge);
                }
            );
        } else if (
            edgeShape === "polyline" &&
            seriesScope.layout === "orthogonal"
        ) {
            graphic.updateProps(
                edge,
                {
                    shape: {
                        parentPoint: [sourceLayout.x, sourceLayout.y],
                        childPoints: [[sourceLayout.x, sourceLayout.y]],
                    },
                    style: {
                        opacity: 0,
                    },
                },
                seriesModel,
                function () {
                    group.remove(edge);
                }
            );
        }
    }
}

function getEdgeShape(seriesScope, sourceLayout, targetLayout) {
    var cpx1;
    var cpy1;
    var cpx2;
    var cpy2;
    var orient = seriesScope.orient;
    var x1;
    var x2;
    var y1;
    var y2;

    if (seriesScope.layout === "radial") {
        x1 = sourceLayout.rawX;
        y1 = sourceLayout.rawY;
        x2 = targetLayout.rawX;
        y2 = targetLayout.rawY;
        var radialCoor1 = radialCoordinate(x1, y1);
        var radialCoor2 = radialCoordinate(
            x1,
            y1 + (y2 - y1) * seriesScope.curvature
        );
        var radialCoor3 = radialCoordinate(
            x2,
            y2 + (y1 - y2) * seriesScope.curvature
        );
        var radialCoor4 = radialCoordinate(x2, y2);
        return {
            x1: radialCoor1.x,
            y1: radialCoor1.y,
            x2: radialCoor4.x,
            y2: radialCoor4.y,
            cpx1: radialCoor2.x,
            cpy1: radialCoor2.y,
            cpx2: radialCoor3.x,
            cpy2: radialCoor3.y,
        };
    } else {
        x1 = sourceLayout.x;
        y1 = sourceLayout.y;
        x2 = targetLayout.x;
        y2 = targetLayout.y;

        if (orient === "LR" || orient === "RL") {
            cpx1 = x1 + (x2 - x1) * seriesScope.curvature;
            cpy1 = y1;
            cpx2 = x2 + (x1 - x2) * seriesScope.curvature;
            cpy2 = y2;
        }

        if (orient === "TB" || orient === "BT") {
            cpx1 = x1;
            cpy1 = y1 + (y2 - y1) * seriesScope.curvature;
            cpx2 = x2;
            cpy2 = y2 + (y1 - y2) * seriesScope.curvature;
        }
    }

    return {
        x1: x1,
        y1: y1,
        x2: x2,
        y2: y2,
        cpx1: cpx1,
        cpy1: cpy1,
        cpx2: cpx2,
        cpy2: cpy2,
    };
}

module.exports = _default;
